Skip to main content

Overview

DevolutionSync uses two complementary models for user management:
  • UsuarioModel - Handles user creation, validation, and listing
  • AuthModel - Handles authentication and credential verification
Both models interact with the usuarios database table, which stores user credentials and role information. Database Table: usuarios Key Responsibilities:
  • User registration and validation
  • Authentication and login
  • Role-based access control (GRADO system)
  • User listing for administration

UsuarioModel

Constructor

public function __construct()
Initializes the model and establishes database connection using Conexion::Conectar().

existeUsuario()

public function existeUsuario($usr)
Checks if a username already exists in the system. Used during registration to prevent duplicate usernames.
usr
string
required
Username to check for existence
Returns: bool - true if username exists, false otherwise
SELECT USR FROM usuarios WHERE USR = ?

guardar()

public function guardar($usr, $pas, $nombre, $grado)
Creates a new user account in the system. Typically called by administrators to register new auxiliary users or other administrators.
usr
string
required
Username for login (must be unique)
pas
string
required
User password (should be hashed before calling this method)
nombre
string
required
Full name or display name of the user
grado
int
required
User role/permission level:
  • 1 = Administrator (full access)
  • 2 = Auxiliary (can register returns)
  • 3 = Consultation (read-only access)
Returns: bool - true on successful insertion, false on failure
INSERT INTO usuarios (USR, PAS, NOMBRE, GRADO) 
VALUES (?, ?, ?, ?)
Security Critical: Always hash passwords using password_hash() before passing them to guardar(). Never store plaintext passwords. The model does not perform hashing internally - this is the caller’s responsibility.

listarTodos()

public function listarTodos()
Retrieves all users from the system, ordered by role (GRADO) and then by name. Used for user management interfaces and administrative dashboards. Excludes password fields for security. Returns: array - Array of associative arrays with user information
USR
string
Username (login identifier)
NOMBRE
string
User’s full name or display name
GRADO
int
User role: 1 (Admin), 2 (Auxiliary), 3 (Consultation)
SELECT USR, NOMBRE, GRADO 
FROM usuarios 
ORDER BY GRADO, NOMBRE
The query intentionally excludes the PAS field to prevent accidental password exposure. Passwords should never be retrieved or displayed in user listing interfaces.

AuthModel

Constructor

public function __construct()
Initializes the model and establishes database connection using Conexion::Conectar().

buscarUsuario()

public function buscarUsuario($username)
Retrieves complete user information including hashed password for authentication purposes. This method should only be used during login flows.
username
string
required
Username to authenticate
Returns: array|false - Associative array with user data, or false if user not found
USR
string
Username (matches search parameter)
PAS
string
Hashed password (use with password_verify())
GRADO
int
User role: 1 (Admin), 2 (Auxiliary), 3 (Consultation)
NOMBRE
string
User’s full name or display name
SELECT USR, PAS, GRADO, NOMBRE 
FROM usuarios 
WHERE USR = ?
Security Critical:
  • Always use password_verify() to check passwords - never compare hashes directly
  • Never log or display password hashes
  • Implement rate limiting on login attempts to prevent brute force attacks
  • Use HTTPS for all login forms to prevent credential interception

Database Schema

Table: usuarios
ColumnTypeDescriptionConstraints
USRVARCHAR(50)Username for loginPRIMARY KEY, UNIQUE
PASVARCHAR(255)Hashed password (bcrypt)NOT NULL
NOMBREVARCHAR(100)Full name or display nameNOT NULL
GRADOINTRole/permission levelNOT NULL, CHECK (1-3)

GRADO Values

The GRADO field implements role-based access control:
GRADORole NamePermissions
1AdministradorFull system access: approve/reject returns, manage users, view all statistics
2AuxiliarRegister returns, upload evidence, view own submissions
3ConsultaRead-only access: view return history, view statistics (no modifications)
To extend the role system beyond three levels:
  1. Add new GRADO values (e.g., 4 = Warehouse Manager, 5 = Finance)
  2. Update authorization middleware to check specific permissions
  3. Create permission mapping:
class Permissions {
    const ROLES = [
        1 => ['admin', 'review', 'create', 'view'],
        2 => ['create', 'view_own'],
        3 => ['view'],
        4 => ['review', 'create', 'view'],  // New: Warehouse
        5 => ['approve_finance', 'view']    // New: Finance
    ];
    
    public static function can($userGrado, $action) {
        return in_array($action, self::ROLES[$userGrado] ?? []);
    }
}

// Usage
if (Permissions::can($_SESSION['grado'], 'approve_finance')) {
    // Allow financial approval
}

Password Security Best Practices

Hashing Passwords

// ✅ CORRECT: Use password_hash with bcrypt
$hashedPassword = password_hash($plainPassword, PASSWORD_BCRYPT);
$model->guardar($username, $hashedPassword, $nombre, $grado);

// ❌ WRONG: Never store plaintext
$model->guardar($username, $plainPassword, $nombre, $grado);

// ❌ WRONG: Never use MD5 or SHA1
$hashedPassword = md5($plainPassword);

Verifying Passwords

// ✅ CORRECT: Use password_verify
if (password_verify($inputPassword, $user['PAS'])) {
    // Login successful
}

// ❌ WRONG: Never compare hashes directly
if (password_hash($inputPassword, PASSWORD_BCRYPT) === $user['PAS']) {
    // This will NEVER work - bcrypt generates different salts
}

Password Requirements

function validatePassword($password) {
    $errors = [];
    
    if (strlen($password) < 8) {
        $errors[] = 'La contraseña debe tener al menos 8 caracteres';
    }
    if (!preg_match('/[A-Z]/', $password)) {
        $errors[] = 'Debe contener al menos una letra mayúscula';
    }
    if (!preg_match('/[a-z]/', $password)) {
        $errors[] = 'Debe contener al menos una letra minúscula';
    }
    if (!preg_match('/[0-9]/', $password)) {
        $errors[] = 'Debe contener al menos un número';
    }
    
    return $errors;
}

// Usage
$errors = validatePassword($_POST['password']);
if (!empty($errors)) {
    echo json_encode(['success' => false, 'errores' => $errors]);
    exit;
}

Complete Authentication Flow

// 1. Login Form Submission
if ($_POST['accion'] === 'login') {
    $username = trim($_POST['username']);
    $password = $_POST['password'];
    
    // Find user
    $authModel = new AuthModel();
    $usuario = $authModel->buscarUsuario($username);
    
    // Verify credentials
    if ($usuario && password_verify($password, $usuario['PAS'])) {
        // Start session
        session_start();
        session_regenerate_id(true); // Prevent session fixation
        
        $_SESSION['usuario'] = $usuario['USR'];
        $_SESSION['nombre'] = $usuario['NOMBRE'];
        $_SESSION['grado'] = $usuario['GRADO'];
        $_SESSION['login_time'] = time();
        
        // Redirect based on role
        $redirect = match($usuario['GRADO']) {
            1 => '/admin/dashboard.php',
            2 => '/auxiliar/registro.php',
            3 => '/consulta/historial.php',
            default => '/index.php'
        };
        
        echo json_encode(['success' => true, 'redirect' => $redirect]);
    } else {
        // Log failed attempt
        error_log("Failed login attempt for user: {$username}");
        
        echo json_encode([
            'success' => false,
            'mensaje' => 'Credenciales incorrectas'
        ]);
    }
}

// 2. Protected Page Check
session_start();

if (!isset($_SESSION['usuario'])) {
    header('Location: /login.php');
    exit;
}

// Check role permissions
if ($_SESSION['grado'] > 2) { // Requires Auxiliary or Admin
    header('Location: /403.php');
    exit;
}

// Session timeout (30 minutes)
if (time() - $_SESSION['login_time'] > 1800) {
    session_destroy();
    header('Location: /login.php?timeout=1');
    exit;
}

// Update last activity
$_SESSION['login_time'] = time();

  • DevolucionModel - Uses usuario_creador and usuario_revisor fields that reference usuarios
  • ProductoModel - Products accessed by authenticated users

Security Checklist

Production Security Requirements:
  • All passwords hashed with password_hash(PASSWORD_BCRYPT)
  • Login form uses HTTPS (SSL/TLS certificate installed)
  • Session cookies have httponly and secure flags
  • Rate limiting implemented on login endpoint
  • Failed login attempts logged for security monitoring
  • Session timeout configured (recommended: 30 minutes)
  • Password complexity requirements enforced
  • SQL injection prevented via prepared statements (already implemented)
  • XSS protection via output escaping (htmlspecialchars())
  • CSRF tokens implemented on all forms